<?php
/**
 *  The properties are stored in the __dict__:
 *  - to get a model's property, simple offsetGet() is OK
 *  - to iterate over properties, iterate over the model
 */
abstract class Yz_Model_ActiveRecord
    extends Yz_Model_Abstract
{
    /** The class name of model's items
     *  @var string
     */
    protected $itemClass;

    /** The list of model's items
     *  @var array
     */
#    protected $items;
    protected $items = array();

    /** The name of the source used by the model
     *  @var string|null
     */
    protected $sourceName;

    /** The parent model, if any
     *  @var Yz_Model_Interface|null
     */
    protected $parentModel;


    /****************************
     *      Public methods      *
     ****************************/

    /** get Model's parent model
     *  @return string
     */
    final public function getParentModel()
    {
        debug();
        return $this->parentModel;
    }

    /** get Model's source name
     *  @return string
     */
    public function getSourceName()
    {
        debug();
        $sName =& $this->sourceName;
        if (! $sName) {
            $sName = strtolower($this->module->getFullName());
            $me = $this->myReflection();
            if ($me->hasConstant('SOURCE_NAME'))
                $sName = str_replace('~', $sName, $me->getConstant('SOURCE_NAME'));
        }
        return $sName;
    }


    /** get the associated item class
     *  @return string class(Yz_Item_Interface)
     */
    public function getItemClass()
    {
        debug();
        return $this->itemClass;
    }


    /** create new item object
     *  @return Yz_Item_Interface
     */
    public function newItem($id=null)
    {
        debug();
        if ($id)
            $id = $this->__dict__['id']->testValue($id);
        $item = new $this->itemClass($this, $id);
        if ($id)
            $this->items[$id] = $item;
        return $item;
    }


    /** fetch item from the storage source
     *      Parameters:
     *  @param id $id
     *  @return Yz_Item_Interface
     */
    public function fetchItemById($id)
    {
        debug();
        $id = $this->__dict__['id']->testValue($id);
        if (isset($this->items[$id]) and ! $this->items[$id]->isEmpty())
            return $this->items[$id];
        $action = $this->selectAction('SelectById');
        $values = $action
            ->setId($id)
            ->perform()
            ->fetch()
        ;
        return $this->itemFromValues($values);
    }


    /** fetch item from the storage source
     *      Parameters:
     *  @param varargs args
     *  @return Yz_Array{Yz_Item_Interface}
     */
    public function fetchList()
    {
        debug();
        $action = $this->selectAction('SelectList');
        $values = $action
            ->perform()
            ->fetch()
        ;
        return $this->assignList($values);
    }


    /** fetch item from the storage source
     *      Parameters:
     *  @param Yz_Item_Interface|Yz_List args
     *  @return Yz_Array{Yz_Item_Interface}
     */
    public function fetchListByKey($keyName, $args)
    {
        debug();
        if (! is_string($keyName))
            throw TypeError::_new_();
        // 1. identify which foreign key references the item
        if (! isset($this->__dict__[$keyName]))
            throw KeyError::_new_(array('key' => $keyName));

        // TODO add model name if key belongs to parent model
        $keyName = $this->name . '.' . $keyName . 'Id';
        // 2. identify the key value[s]
        if ($args instanceof Yz_Item_Interface) {
            assert('$args->id > 0');
            $keys = array($args->id);
        } else if ($args instanceof Yz_List) {
            $keys = array_keys($keys->toArray());
        } else {
            throw TypeError::_new_();
        }
        $action = $this->selectAction('SelectListByKey');
        $values = $action
            ->setKey($keyName, $keys)
            ->perform()
            ->fetch()
        ;
        return $this->assignList($values);
    }

    /**
     *  fetch hash of items indexed by id, using pagination (p8n)
     *  @param Yz_Pagination_Interface
     *  @return Yz_Hash[Yz_Item_Interface]
     *  @throw NotFound
     */
    public function fetchPage(Yz_Pagination_Interface $p8n)
    {
        debug();
        $action = $this->selectAction('SelectList');
        $values = $action
            ->setOffset($p8n->getPageOffset())
            ->setLimit($p8n->getPageItems())
            ->perform()
            ->fetch()
        ;
        $p8n->setItemCount($action->getFoundRows());
        return $this->assignList($values);
    }

    /** fetch item's neighbors as array (0: previous, 1: next)
     *  @param Yz_Item_Interface
     *  @return array(Yz_Item_Interface)
     */
    public function fetchNeighbors(Yz_Item_Interface $item)
    {
        debug();
        assert('$item instanceof $this->itemClass and isset($this->items[$item->id])');
        $action = $this->selectAction('SelectNeighbors');
        $values = $action
            ->setItem($item)
            ->perform()
            ->fetch()
        ;
        $neighbors = array(null, null);
        if ($values) {
            foreach ($values as $v) {
                $itemN = $this->itemFromValues($v);
                $i = $itemN->id < $item->id ? 0 : 1;
                $neighbors[$i] = $itemN;
            }
        }
        return $neighbors;
    }


    /********************************
     *      Item internal methods   *
     ********************************/


    /** Delete item from the storage source
     *  NOTE: Designed to be called internally by item
     *      Do not use this method; use Item's delete()
     *  @todo check constraint for number of items
     *  @param Yz_Item_Interface
     *  @return this
     */
    public function deleteItem(Yz_Item_Interface $item)
    {
        debug();
        assert('$item instanceof $this->itemClass and isset($this->items[$item->id])');
        $action = $this
            ->selectAction('DeleteItem')
            ->setItem($item)
            ->perform()
        ;
        unset($this->items[$item->id]);
        return $this;
    }

    /** Add item to the storage source
     *  NOTE: Designed to be called internally by item
     *      Do not use this method; use Item's save()
     *  @todo check constraint for number of items
     *  @param Yz_Item_Interface
     *  @return this
     */
    public function addItem(Yz_Item_Interface $item)
    {
        debug();
        assert('$item instanceof $this->itemClass and $item->id == 0');
        $action = $this
            ->selectAction('InsertItem')
            ->setItem($item)
            ->perform()
        ;
        $id = $action->getInsertId();
        assert('is_int($id) and $id > 0');
        $item->setId($id);
        $this->items[$id] = $item;
        return $this;
    }

    /** Add item to the storage source
     *  NOTE: Designed to be called internally by item
     *      Do not use this method; use Item's save()
     *  @todo check constraint for number of items
     *  @param Yz_Item_Interface
     *  @return this
     */
    public function updateItem(Yz_Item_Interface $item)
    {
        debug();
        assert('$item instanceof $this->itemClass and isset($this->items[$item->id])');
        $action = $this
            ->selectAction('UpdateItem')
            ->setItem($item)
            ->perform()
        ;
        return $this;
    }

    /********************************
     *      Protected methods       *
     ********************************/

    /** Assign values from the database to an item
     *  @param Yz_Hash values { "ModelName_PropName" : mixed value }
     *  @return Yz_List(Yz_Item_Interface)
     */
    public function assignList($values)
    {
        $list = new Yz_List($this->itemClass);
        foreach ($values as $row) {
            $item = $this->itemFromValues($row);
            $list[$item->id] = $item;
        }
        return $list;
    }


    protected function init()
    {
        $this->itemClass = $this->module->resolveClass('Item_' . $this->name);
###        $this->items = new Yz_Item_List($this);
        // subclasses put their property init here
        return $this;
    }


    /** create new item object from values
     *  @return Yz_Item_Interface
     */
    public function itemFromValues($values)
    {
        debug($this);
        $parentItem = null;
        if ($this->parentModel) {
            $parentName = '_'.$this->parentModel->name;
            $pv = $values[$parentName];
            $parentItem = $this->parentModel->itemFromValues($pv);
            $id = $parentItem->id;
            $values[$parentName] = $parentItem;
        } else {
            $id = $values['id'];
            assert('is_numeric($id) and $id > 0');
        }
        if (isset($this->items[$id]))
            // assume that any referenced items have also been assigned
            $item = $this->items[$id];
        else
            $this->items[$id] = $item = new $this->itemClass($this, $id);

        // item can be empty even if it was present in list
        if ($item->isEmpty()) {
            $item->assignValues($values, $parentItem);

            // NOTE: if the parent model's item with the ID belongs to this model,
            // it NECESSARILY exists in this model's list
            // (in case the item was fetched via parent model, its type will be that of parent)
            // TODO instead of directly assigning item to parent's list, if item is of parent type,
            // add a reference to this item, so that consistency is not broken
            // (when modifying parent, this item must also get modified!)
        }
        return $item;
    }

    protected function extend()
    {
        $parentClass = get_parent_class($this);
        debug("Extending $parentClass");
        $parentModel = $parentClass::getInstance();
        //$parentModel->__dict__['_child' . $this->name]
        $this->__dict__['_'.$parentModel->name] = new Yz_Property_OneToOne($parentModel);
        $this->parentModel = $parentModel;
    }


}
